/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package andreafrancia.util;

import java.beans.ExceptionListener;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 *
 * @author Andrea Francia andrea.francia at gmx dot org
 */
public class AdapterFactory {

    /**
     * Return {@code true} if the specified {@see Method} can throw the specified
     * {@code throwable}, {@code false} otherwise.
     * 
     * @param method
     * @param throwable
     * @return {@code true} if the specified {@see Method} can throw the specified
     * {@see Throwable}, {@code false} otherwise.
     */
    protected static final boolean canThrow(Method method, Class<? extends Throwable> throwable) {
        if(RuntimeException.class.isAssignableFrom(throwable)
                || Error.class.isAssignableFrom(throwable)) {
            return true;
        }
        for(Class<?> i : method.getExceptionTypes()) {
            if(i.isAssignableFrom(throwable)) {
                return true;
            }
        }
        return false;
    }
            
            
    /**
     * Create an adapter that adapt the {@code adaptee} object to the 
     * {@code adaptingInterface}.
     * 
     * All method called on the {@code adapter} are called on the 
     * {@code adaptee}. 
     * If a call throw a exception undeclared by the adaptingInterface the 
     * exception is logged through the {@see ExceptionListener} and the value 
     * returned by the same method of the {@code fallback} object is returned.
     * 
     * @param <I> the adaptingInterface
     * @param adaptingInterface 
     * @param adaptee the object which is adapted
     * @param fallback when an unthrowable exception occours this is called.
     * @param exceptionListener listen for unthrowable exceptions.
     * @return an adapter implementing the adaptingInterface
     * @throws java.lang.NoSuchMethodException if there is any method in the 
     * adaptingInterface that can not be called on the adaptee.
     * @throws IllegalArgumentException if the fallback is null.
     */
    public static <I> I adapt(final Class<I> adaptingInterface,
                                final Object adaptee, 
                                final I fallback,
                                final ExceptionListener exceptionListener) 
            throws NoSuchMethodException 
    {
        // for each target method find the relevant adaptee method
        final Map<Method, Method> adapteeMethods = new HashMap<Method, Method>();
        
        for (Method adaptedMethod : adaptingInterface.getMethods()) {
            Method adapteeMethod = adaptee.getClass()
                    .getMethod(adaptedMethod.getName(),
                               adaptedMethod.getParameterTypes());
            adapteeMethods.put(adaptedMethod, adapteeMethod);
        }
        
        if(fallback == null) {
            throw new IllegalArgumentException("Parameter fallback should not be null");
        }
        
        InvocationHandler ih = new InvocationHandler() {
            public Object invoke(Object proxy, Method adaptedMethod, Object[] args) 
                    throws Throwable 
            {
                try {
                    // call the relevant adaptee method
                    Method adapteeMethod = adapteeMethods.get(adaptedMethod);
                    return adapteeMethod.invoke(adaptee, args);
                } catch (InvocationTargetException e) {
                    if(e.getCause() instanceof Exception) {
                        Exception cause = (Exception) e.getCause();
                        if(canThrow(adaptedMethod, cause.getClass() )) {
                            throw e.getCause();
                        } else {
                            // exception is not expected by target classes
                            // so we notify the listener
                            exceptionListener.exceptionThrown(cause);
                            return adaptedMethod.invoke(fallback, args);
                        }
                    } else {
                        throw e.getCause();
                    }
                }
            }
        };
        
        @SuppressWarnings("unchecked")
        I adapter = (I) Proxy.newProxyInstance(
                adaptingInterface.getClassLoader(), new Class[]{adaptingInterface}, ih);
        
        return adapter;
    }

    public static <I> I adapt(final Class<I> adaptingInterface,
                                final Object adaptee, 
                                final I fallback)
        throws NoSuchMethodException 
    {
        return adapt(adaptingInterface, adaptee, fallback, null);
    }

}
